﻿using System.Collections.Generic;
using System.Linq;
using Affinity.Core.Utility;
using Affinity.Core.Validation;

namespace Affinity.Synthesis.Collections
{
    /// <summary>
    /// List extension methods.
    /// </summary>
    public static class ListExtensions
    {
        /// <summary>
        /// Put inside a lambda binding expression to optimize handling of changing result sets.
        /// 
        /// For example:
        /// 
        /// BindLambda("SomeFilteredCollection", () => new ObservableCollection&gt;ItemType&lt;().DistinctFor(SomeUnfilteredCollection.Where(item => item.IsRelevant)));
        /// This creates an ObservableCollection only once, then updates it when the filtered source value changes (this happens when SomeUnfilteredCollection changes 
        /// or the property IsRelevant changes on an item in SomeUnfilteredCollection).
        /// </summary>
        public static TList DistinctFor<TList, TItem>(this TList list, IEnumerable<TItem> source)
            where TList : class, IList<TItem>
        {
            Assert.ArgumentNotNull(list, "list");
            if (source == null) source = new TItem[0];
            else source = source.Distinct().ToArray();

            // Avoid modifying the collection during its own change event.
            Threading.ExecuteOnUIThread(true, () =>
                {
                    List<TItem> sourceList = source.ToList();
                    TItem[] addedItems = sourceList.Except(list).ToArray();
                    TItem[] removedItems = list.Except(sourceList).ToArray();

                    foreach (TItem removedItem in removedItems) list.Remove(removedItem);

                    // Add items in order of ascending index to avoid inserting beyond the end of the target list.
                    foreach (var addedItemWithIndex in 
                        addedItems
                        .Select(addedItem => new { AddedItem = addedItem, Index = sourceList.IndexOf(addedItem) })
                        .OrderBy(pair => pair.Index))
                    {
                        list.Insert(addedItemWithIndex.Index, addedItemWithIndex.AddedItem);
                    }
                });

            return list;
        }
    }
}
